package cn.redragon.soa.service;

import static io.vavr.API.$;
import static io.vavr.API.Case;
import static io.vavr.API.Match;
import static io.vavr.API.run;

import cn.redragon.soa.amqp.handler.StockEventSender;
import cn.redragon.soa.amqp.model.StockImportedEvent;
import cn.redragon.soa.amqp.model.StockUpdatedEvent;
import cn.redragon.soa.common.constant.Constants;
import cn.redragon.soa.common.constant.RedisCacheKey;
import cn.redragon.soa.common.enums.StockActionType;
import cn.redragon.soa.common.enums.StockImportMode;
import cn.redragon.soa.common.exception.ServiceException;
import cn.redragon.soa.common.exception.StockError;
import cn.redragon.soa.common.exception.SystemError;
import cn.redragon.soa.common.service.RedisService;
import cn.redragon.soa.common.util.JsonUtil;
import cn.redragon.soa.model.dto.stock.ProductDTO;
import cn.redragon.soa.model.dto.stock.ReservationDTO;
import cn.redragon.soa.model.dto.stock.StockDTO;
import cn.redragon.soa.model.dto.stock.StockHistoryDTO;
import cn.redragon.soa.model.dto.stock.StockReservedDTO;
import cn.redragon.soa.util.StockUtil;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class StockService {

    private final StockUtil stockUtil;

    private final StockEventSender stockEventSender;

    private final RedisService redisService;

    @Value("${spring.redis.expire-database}")
    private Integer expireDb;

    public List<StockDTO> getStocks(String brand, List<ProductDTO> productDTOs) {
        List<StockDTO> stocks = Lists.newArrayList();
        productDTOs.forEach(p -> stocks.add(loadStock(brand, p)));
        return stocks;
    }

    public Map<String, StockDTO> getStocksMap(String brand, List<ProductDTO> productDTOs) {
        return productDTOs.stream().map(p -> loadStock(brand, p)).collect(
                Collectors.toMap(StockDTO::getSku, Function.identity()));
    }

    public Map<String, String> getTotalStocks(String brand, String type) {
        String totalStockKey = String.format(RedisCacheKey.SKU_TOTAL_STOCK, brand, type);
        return stockUtil.hashScan(totalStockKey);
    }

    public void checkStocks(String brand, List<StockReservedDTO> stocks) {
        String result = stockUtil.checkStock(brand, stocks);
        if (!Constants.STATUS_SUCCESS.equals(result)) {
            String type = stocks.iterator().next().getType();
            throw newServiceExceptionWithData(brand, type, result, StockError.OUT_OF_STOCK);
        }
    }

    public void reserveStocks(String brand, ReservationDTO reservationDTO, String profile) {
        String result = null;
        try {
            result = stockUtil.reserveStock(brand, reservationDTO.getStocks());
            if (!Constants.STATUS_SUCCESS.equals(result)) {
                throw newServiceExceptionWithData(brand, reservationDTO.getType(), result, StockError.OUT_OF_STOCK);
            }
            if (reservationDTO.getTtl() != null && reservationDTO.getTtl() > 0) {
                stockUtil.selectAndSetEx(expireDb, String
                                             .format(RedisCacheKey.RESERVATION_EXPIRED, brand, reservationDTO.getType(), reservationDTO.getCustomerId(),
                                                     reservationDTO.getOrderNumber()),
                                         Boolean.TRUE.toString(), reservationDTO.getTtl());

            }
        } finally {
            sendStockUpdatedEvent(brand, reservationDTO, StockActionType.PLACE_ORDER, Constants.STATUS_SUCCESS.equals(result), profile);
        }
    }

    public void returnStocks(String brand, ReservationDTO reservationDTO, String profile) {
        List<StockReservedDTO> stockDTOs = reservationDTO.getStocks();
        stockDTOs.stream().filter(s -> s.getRemarks() == null).forEach(s -> s.setRemarks("Return released stocks"));
        String result = null;
        try {
            result = stockUtil.importStockIncremental(brand, stockDTOs);
            if (!Constants.STATUS_SUCCESS.equals(result)) {
                throw new ServiceException(StockError.IMPORT_STOCK_GENERIC_ERROR);
            }
        } finally {
            this.sendStockUpdatedEvent(brand, reservationDTO, StockActionType.IMPORT, Constants.STATUS_SUCCESS.equals(result), profile, StockImportMode.RETURN);
        }
    }

    public void cancelStocks(String brand, String type, String customerId, String orderNumber, String profile, StockActionType actionType) {
        ReservationDTO reservationDTO = this.getCachedReservation(brand, type, customerId, orderNumber);
        String result = null;
        try {
            if (!StockActionType.CLOSE_TYPES.contains(actionType)) {
                String value = SystemError.INVALID_PARAM_ERROR.withParams(actionType.name(), "Invalid action type");
                log.error(value);
                throw new IllegalArgumentException(value);
            }
            result = stockUtil.cancelStock(brand, reservationDTO.getStocks());
            if (!Constants.STATUS_SUCCESS.equals(result)) {
                throw newServiceExceptionWithData(brand, reservationDTO.getType(), result, StockError.CLOSE_STOCK_ERROR);
            }
            if (actionType == StockActionType.EXPIRE_ORDER && reservationDTO.getTtl() != null && reservationDTO.getTtl() > 0) {
                String maCustomerKey = this.getMaCustomerHashKey(brand, customerId);
                String maCustomerReservedField = this.getHashReservedField(type, orderNumber);
                //remove reserved record for ma customer
                redisService.hashDel(maCustomerKey, maCustomerReservedField);
            }
        } finally {
            sendStockUpdatedEvent(brand, reservationDTO, actionType, Constants.STATUS_SUCCESS.equals(result), profile);
        }
    }

    public void releaseStocks(String brand, String type, String customerId, String orderNumber, String profile) {
        ReservationDTO reservationDTO = this.getCachedReservation(brand, type, customerId, orderNumber);
        String result = null;
        try {
            result = stockUtil.releaseStock(brand, reservationDTO.getStocks());
            if (!Constants.STATUS_SUCCESS.equals(result)) {
                throw newServiceExceptionWithData(brand, reservationDTO.getType(), result, StockError.RELEASE_STOCK_ERROR);
            }

            if (reservationDTO.getTtl() != null && reservationDTO.getTtl() > 0) {
                stockUtil.selectAndDel(expireDb, String.format(RedisCacheKey.RESERVATION_EXPIRED, brand, reservationDTO.getType(), customerId, orderNumber),
                                       Boolean.TRUE.toString());
            }

        } finally {
            sendStockUpdatedEvent(brand, reservationDTO, StockActionType.EXPORT, Constants.STATUS_SUCCESS.equals(result), profile);
        }
    }

    public void importStocks(String brand, StockImportedEvent importedEvent, StockActionType actionType, String profile, StockImportMode mode) {
        List<StockReservedDTO> stockDTOs = new ArrayList<>();
        String result = null;
        try {
            stockDTOs = importedEvent.getStocks().stream().map(m -> convertToStockReservedDTO(brand, m)).collect(Collectors.toList());
            if (StockImportMode.INCREMENTAL == mode) {
                result = stockUtil.importStockIncremental(brand, stockDTOs);
            } else {
                result = stockUtil.importStock(brand, stockDTOs);
            }

            if (!Constants.STATUS_SUCCESS.equals(result)) {
                throw new ServiceException(StockError.IMPORT_STOCK_GENERIC_ERROR);
            }
        } finally {
            this.sendStockUpdatedEvent(brand, ReservationDTO.builder().stocks(stockDTOs).build(), actionType,
                                       Constants.STATUS_SUCCESS.equals(result), profile, mode);
        }
    }

    public ReservationDTO getCachedReservation(String brand, String type, String customerId, String orderNumber) {
        String cachedReservationStr = redisService.hashGet(getMaCustomerHashKey(brand, customerId), getHashReservedField(type, orderNumber));
        if (cachedReservationStr == null) {
            throw new ServiceException(StockError.MISSING_RESERVATION, type, orderNumber, customerId);
        }
        ReservationDTO cachedReservation = JsonUtil.jsonToObject(cachedReservationStr, ReservationDTO.class);
        cachedReservation.setCustomerId(customerId);
        cachedReservation.setOrderNumber(orderNumber);
        cachedReservation.setType(type);
        return cachedReservation;
    }

    private ServiceException newServiceExceptionWithData(final String brand, final String type, final String result, final StockError outOfStock) {
        String[] errSkus = StringUtils.removeEnd(
            StringUtils.replace(result, String.format(RedisCacheKey.SKU_STOCK, brand, type, Constants.EMPTY), Constants.EMPTY),
            Constants.Separator.SEMI_COLON)
            .split(Constants.Separator.SEMI_COLON);
        return new ServiceException(errSkus, outOfStock, result);
    }

    private void sendStockUpdatedEvent(final String brand, final ReservationDTO reservationDTO, final StockActionType actionType,
                                       final Boolean isSuccess, final String profile) {
        this.sendStockUpdatedEvent(brand, reservationDTO, actionType, isSuccess, profile, StockImportMode.INCREMENTAL);
    }

    private void sendStockUpdatedEvent(final String brand, final ReservationDTO reservationDTO, final StockActionType actionType,
                                       final Boolean isSuccess, final String profile, StockImportMode mode) {
        List<StockHistoryDTO> historyDTOs = Lists.newArrayList();
        reservationDTO.getStocks().forEach(
            i -> {
                ProductDTO productDTO = ProductDTO.builder().sku(i.getSku()).spu(i.getSpu()).type(i.getType()).build();
                StockDTO stock = this.loadStock(brand, productDTO);
                final StockHistoryDTO history = StockHistoryDTO.builder().available(
                    stock.getAvailable()).reserved(stock.getReserved()).orderNumber(reservationDTO.getOrderNumber()).customerId(reservationDTO.getCustomerId())
                    .action(actionType).sku(i.getSku()).spu(i.getSpu()).creator(profile).remarks(i.getRemarks()).isSuccess(isSuccess).type(i.getType())
                    .build();
                Match(actionType).of(
                    Case($(StockActionType.IMPORT), o -> run(() -> {
                        history.setImported(i.getQty());
                        if (isSuccess) {
                            if (StockImportMode.INCREMENTAL == mode) {
                                stockUtil.hashIncrement(String.format(RedisCacheKey.SKU_TOTAL_STOCK, brand, productDTO.getType()), productDTO.getSku(), i.getQty());
                            } else if (StockImportMode.FULL == mode) {
                                stockUtil.hashSet(String.format(RedisCacheKey.SKU_TOTAL_STOCK, brand, productDTO.getType()), productDTO.getSku(), i.getQty());
                            }
                        }
                    })),
                    Case($(), o -> run(() -> history.setRequested(i.getQty())))
                );
                historyDTOs.add(history);

            }
        );

        stockEventSender.triggerStockHistory(brand, StockUpdatedEvent.builder().histories(historyDTOs).build());
    }

    private StockReservedDTO convertToStockReservedDTO(String brand, StockImportedEvent.StockImportedItem item) {
        StockDTO stockDto = loadStock(brand, ProductDTO.builder().sku(item.getSku()).type(item.getType()).build());
        log.info("stocks for sku {} before importing: {}", stockDto.getSku(), stockDto.toString());
        return StockReservedDTO.builder().qty(item.getQty()).sku(item.getSku()).type(item.getType()).remarks(item.getRemarks()).build();
    }

    private StockDTO loadStock(String brand, ProductDTO productDTO) {
        String stockKey = String.format(RedisCacheKey.SKU_STOCK, brand, productDTO.getType(), productDTO.getSku());
        Long stock = stockUtil.hashGetLong(stockKey, stockKey);
        Long reservedStock = stockUtil.hashGetLong(stockKey, stockKey.concat(RedisCacheKey.SKU_RESERVED));
        String totalStockKey = String.format(RedisCacheKey.SKU_TOTAL_STOCK, brand, productDTO.getType());
        Long totalStock = stockUtil.hashGetLong(totalStockKey, productDTO.getSku());
        return StockDTO.builder().reserved(reservedStock).available(stock).sku(productDTO.getSku()).spu(productDTO.getSpu()).type(productDTO.getType()).total(
            totalStock).build();
    }

    private String getMaCustomerHashKey(String brand, String customerId) {
        return String.format(RedisCacheKey.MA_CUSTOMER_PROFILE, brand, customerId);
    }

    private String getHashReservedField(String type, String orderNumber) {
        return String.format(RedisCacheKey.HASH_ORDER_RESERVED, type, orderNumber);
    }
}
